using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using IQToolkit.Data.Common;

namespace LinqToVfp {
    internal class VfpParameterizer : VfpExpressionVisitor {
        private QueryLanguage language;
        private Dictionary<TypeAndValue, NamedValueExpression> map = new Dictionary<TypeAndValue, NamedValueExpression>();
        private Dictionary<HashedExpression, NamedValueExpression> pmap = new Dictionary<HashedExpression, NamedValueExpression>();
        private int parameterCounter = 0;

        private VfpParameterizer(QueryLanguage language) {
            this.language = language;
        }

        public static Expression Parameterize(QueryLanguage language, Expression expression) {
            return new VfpParameterizer(language).Visit(expression);
        }

        protected override Expression VisitProjection(ProjectionExpression proj) {
            // don't parameterize the projector or aggregator!
            SelectExpression select = (SelectExpression)this.Visit(proj.Select);
            return this.UpdateProjection(proj, select, proj.Projector, proj.Aggregator);
        }

        protected override Expression VisitBinary(BinaryExpression b) {
            Expression left = this.Visit(b.Left);
            Expression right = this.Visit(b.Right);
            if (left.NodeType == (ExpressionType)DbExpressionType.NamedValue
             && right.NodeType == (ExpressionType)DbExpressionType.Column) {
                NamedValueExpression nv = (NamedValueExpression)left;
                ColumnExpression c = (ColumnExpression)right;
                left = new NamedValueExpression(nv.Name, c.QueryType, nv.Value);
            }
            else if (b.Right.NodeType == (ExpressionType)DbExpressionType.NamedValue
             && b.Left.NodeType == (ExpressionType)DbExpressionType.Column) {
                NamedValueExpression nv = (NamedValueExpression)right;
                ColumnExpression c = (ColumnExpression)left;
                right = new NamedValueExpression(nv.Name, c.QueryType, nv.Value);
            }

            return this.UpdateBinary(b, left, right, b.Conversion, b.IsLiftedToNull, b.Method);
        }

        protected override ColumnAssignment VisitColumnAssignment(ColumnAssignment ca) {
            ca = base.VisitColumnAssignment(ca);
            Expression expression = ca.Expression;
            NamedValueExpression nv = expression as NamedValueExpression;
            if (nv != null) {
                expression = new NamedValueExpression(nv.Name, ca.Column.QueryType, nv.Value);
            }

            return this.UpdateColumnAssignment(ca, ca.Column, expression);
        }

        protected override Expression VisitConstant(ConstantExpression c) {
            if (c.Value != null && !this.IsNumeric(c.Value.GetType())) {
                NamedValueExpression nv;
                TypeAndValue tv = new TypeAndValue(c.Type, c.Value);
                if (!this.map.TryGetValue(tv, out nv)) { // re-use same name-value if same type & value
                    var queryType = this.language.TypeSystem.GetColumnType(c.Type);

                    if (queryType == null) {
                        return c;
                    }
                        string name = "@__Param__" + (this.parameterCounter++) + "__";
                        nv = new NamedValueExpression(name, queryType, c);
                        this.map.Add(tv, nv);
                }

                return nv;
            }

            return c;
        }

        protected override Expression VisitParameter(ParameterExpression p) {
            return this.GetNamedValue(p);
        }

        protected override Expression VisitMemberAccess(MemberExpression m) {
            m = (MemberExpression)base.VisitMemberAccess(m);
            NamedValueExpression nv = m.Expression as NamedValueExpression;
            if (nv != null) {
                Expression x = Expression.MakeMemberAccess(nv.Value, m.Member);
                return this.GetNamedValue(x);
            }

            return m;
        }

        private Expression GetNamedValue(Expression e) {
            NamedValueExpression nv;
            HashedExpression he = new HashedExpression(e);
            if (!this.pmap.TryGetValue(he, out nv)) {
                string name = "@__Param__" + (this.parameterCounter++) + "__";
                nv = new NamedValueExpression(name, this.language.TypeSystem.GetColumnType(e.Type), e);
                this.pmap.Add(he, nv);
            }

            return nv;
        }

        private bool IsNumeric(Type type) {
            switch (Type.GetTypeCode(type)) {
                /*
                 * Removed the following from the switch statement to avoid localization issues.
                 * case TypeCode.Decimal:
                 * case TypeCode.Double:
                 * case TypeCode.Single:
                 */

                case TypeCode.Boolean:
                case TypeCode.Byte:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.SByte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                    return true;
                default:
                    return false;
            }
        }

        private struct TypeAndValue : IEquatable<TypeAndValue> {
            private Type type;
            private object value;
            private int hash;

            public TypeAndValue(Type type, object value) {
                this.type = type;
                this.value = value;
                this.hash = type.GetHashCode() + (value != null ? value.GetHashCode() : 0);
            }

            public override bool Equals(object obj) {
                if (!(obj is TypeAndValue)) {
                    return false;
                }

                return this.Equals((TypeAndValue)obj);
            }

            public bool Equals(TypeAndValue vt) {
                return vt.type == this.type && object.Equals(vt.value, this.value);
            }

            public override int GetHashCode() {
                return this.hash;
            }
        }

        private struct HashedExpression : IEquatable<HashedExpression> {
            private Expression expression;
            private int hashCode;

            public HashedExpression(Expression expression) {
                this.expression = expression;
                this.hashCode = Hasher.ComputeHash(expression);
            }

            public override bool Equals(object obj) {
                if (!(obj is HashedExpression)) {
                    return false;
                }

                return this.Equals((HashedExpression)obj);
            }

            public bool Equals(HashedExpression other) {
                return this.hashCode == other.hashCode &&
                    DbExpressionComparer.AreEqual(this.expression, other.expression);
            }

            public override int GetHashCode() {
                return this.hashCode;
            }

            private class Hasher : VfpExpressionVisitor {
                private int hc;

                internal static int ComputeHash(Expression expression) {
                    var hasher = new Hasher();
                    hasher.Visit(expression);
                    return hasher.hc;
                }

                protected override Expression VisitConstant(ConstantExpression c) {
                    this.hc = this.hc + ((c.Value != null) ? c.Value.GetHashCode() : 0);
                    return c;
                }
            }
        }
    }
}